package com.airbnb.abacus.validation;

import com.airbnb.abacus.api.LedgerAccountType;
import com.airbnb.abacus.api.LedgerDirection;
import com.airbnb.abacus.api.LedgerEntryDetail;
import com.airbnb.abacus.validation.entity.LedgerEntryPair;
import com.google.common.base.Preconditions;
import com.google.inject.Singleton;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.inject.Inject;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Singleton
@RequiredArgsConstructor(onConstructor = @__(@Inject))
public class LedgerEntryValidator {

  public List<LedgerEntryPair> validateAndPairUpLedgerEntry(
      @NonNull List<LedgerEntryDetail> entryList) throws IllegalStateException {
    // classify entries as CREDIT group and DEBIT group, the size of two group must be the same
    // if duplicated token exists, `IllegalStateException` is thrown during `Collectors.toMap`
    final Map<String, LedgerEntryDetail> creditEntries =
        entryList
            .stream()
            .filter(entry -> entry.getDirection() == LedgerDirection.CREDIT)
            .collect(Collectors.toMap(LedgerEntryDetail::getLedgerEntryToken, Function.identity()));
    final Map<String, LedgerEntryDetail> debitEntries =
        entryList
            .stream()
            .filter(entry -> entry.getDirection() == LedgerDirection.DEBIT)
            .collect(Collectors.toMap(LedgerEntryDetail::getLedgerEntryToken, Function.identity()));

    int entriesCount = debitEntries.size();
    Preconditions.checkArgument(
        creditEntries.size() == entriesCount,
        "ledger entries are not well paired, credit count %s, debit count %s",
        String.valueOf(creditEntries.size()),
        String.valueOf(entriesCount));

    // for each debit entry, lookup paired credit entry to create LedgerEntryPair
    List<LedgerEntryPair> entryPairs =
        debitEntries
            .values()
            .stream()
            .map(
                debit -> {
                  String creditEntryToken = debit.getPairLedgerEntryToken();
                  Preconditions.checkArgument(
                      creditEntries.containsKey(creditEntryToken),
                      "no paired credit entry for debit entry %s",
                      debit.getLedgerEntryToken());
                  LedgerEntryDetail credit = creditEntries.get(creditEntryToken);

                  if (LedgerEntryPair.isCurrencyExchangeLedgerPair(credit, debit)) {
                    log.info("filter exchange ledger pair {} {}", debit, credit);
                    return null;
                  }

                  // more validation performed in LedgerEntryPair constructor
                  return LedgerEntryPair.of(debit, credit);
                })
            .filter(Objects::nonNull)
            .collect(Collectors.toList());

    log.debug("created {} entry pairs {}", entryPairs.size(), entryPairs);

    // validate mediation balanced
    long mediation = accumulateMediation(entryPairs);
    Preconditions.checkState(
        mediation == 0,
        "Mediation not balanced for these ledger entries, result is %s",
        String.valueOf(mediation));

    return entryPairs;
  }

  /**
   * compute final mediation amount in some entry pairs, usually check against 0 to determine if
   * mediation is balanced
   *
   * @param entryPairs list of entry pairs
   * @return mediation result
   */
  public long accumulateMediation(@NonNull Collection<LedgerEntryPair> entryPairs)
      throws IllegalStateException {
    return entryPairs
        .stream()
        .filter(Objects::nonNull)
        .mapToLong(
            entry -> {
              if (entry.getCreditAccountType() == LedgerAccountType.MEDIATION) {
                return entry.getCurrencyAmountMicros().getAmountMicros();
              }
              if (entry.getDebitAccountType() == LedgerAccountType.MEDIATION) {
                return -entry.getCurrencyAmountMicros().getAmountMicros();
              }
              // LedgerEntryPairType defines all valid combination of account type and direction. It
              // never goes here if everything is fine
              throw new IllegalStateException(
                  String.format(
                      "invalid LedgerEntryPair %s: neither credit nor debit direction is MEDIATION",
                      entry));
            })
        .sum();
  }
}
